Skip to content

Optimize local contrast#79

Merged
meesoft merged 7 commits into
mainfrom
features/OptimizeLocalContrast
Apr 27, 2026
Merged

Optimize local contrast#79
meesoft merged 7 commits into
mainfrom
features/OptimizeLocalContrast

Conversation

@meesoft
Copy link
Copy Markdown
Owner

@meesoft meesoft commented Apr 18, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Prevented a potential null-reference when converting bitmap data, improving stability.
  • Performance Improvements

    • Faster vertical filtering via more efficient parallel processing.
    • Faster vertical smoothing with SIMD/vectorized processing and fallbacks for remaining data.
  • Tests

    • Added a benchmarking utility to measure and report performance across runs.

@meesoft meesoft marked this pull request as ready for review April 26, 2026 18:53
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 26, 2026

Warning

Rate limit exceeded

@meesoft has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 52 minutes and 30 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 52 minutes and 30 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ced04b88-8bf4-4dec-a483-9449364fe234

📥 Commits

Reviewing files that changed from the base of the PR and between 56420da and 97cd319.

📒 Files selected for processing (2)
  • PhotoLocator/BitmapOperations/IIRSmoothOperation.cs
  • PhotoLocatorTest/BitmapOperations/IncreaseLocalContrastOperationTest.cs

Walkthrough

Adds a null-check to FloatBitmap.ToPixels8(), refactors vertical filtering in IIRMinMaxOperation to process columns in 4-column groups with parallelization, vectorizes vertical smoothing in IIRSmoothOperation using System.Numerics.Vector<float>, and introduces a new PhotoLocator.BenchmarkHelper test utility.

Changes

Cohort / File(s) Summary
Robustness
PhotoLocator/BitmapOperations/FloatBitmap.cs
Added a null-check in ToPixels8(double gamma) that throws InvalidOperationException if Elements is null.
Min/Max filter parallelization
PhotoLocator/BitmapOperations/IIRMinMaxOperation.cs
Rewrote vertical pass to process columns in groups of four using parallel loops with separate recurrence state per column, plus a tail-loop for remaining columns; condensed conditional update/store logic.
Smoothing vectorization
PhotoLocator/BitmapOperations/IIRSmoothOperation.cs
Replaced per-column scalar vertical smoothing with a vectorized implementation using System.Numerics.Vector<float> across column blocks and a scalar tail pass for leftover columns; horizontal pass preserved with local variables.
Benchmarking utility
PhotoLocatorTest/BenchmarkHelper.cs
Added PhotoLocator.BenchmarkHelper.Run(Action action, int innerLoops = 1, int outerIterations = 5) to run timed iterations with GC control, report per-iteration times, and assert inconclusive with min/median results.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The pull request title "Optimize local contrast" is vague and generic, failing to convey the specific technical improvements made across multiple files. Provide a more specific title that reflects the actual changes, such as "Refactor IIR filter operations and add SIMD vectorization for smoothing" or "Optimize image filtering with vectorization and parallel improvements."
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch features/OptimizeLocalContrast

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
PhotoLocatorTest/BenchmarkHelper.cs (1)

6-6: Two small polish items.

  • BenchmarkHelper exposes only static members; mark the class static to prevent instantiation.
  • sw.ElapsedMilliseconds truncates to whole milliseconds, which can hide differences for fast operations; sw.Elapsed.TotalMilliseconds (double) or sw.ElapsedTicks would be more useful for benchmarking. The iterationTimes array would need to become double[]/long[] accordingly.
♻️ Suggested polish
-    public class BenchmarkHelper
+    public static class BenchmarkHelper
     {
         public static void Run(Action action, int innerLoops = 1, int outerIterations = 5)
         {
             ...
-            var iterationTimes = new long[outerIterations];
+            var iterationTimes = new double[outerIterations];
             ...
-                Console.WriteLine($"Iteration {i + 1}: {sw.ElapsedMilliseconds} ms");
+                Console.WriteLine($"Iteration {i + 1}: {sw.Elapsed.TotalMilliseconds:F3} ms");
                 ...
-                iterationTimes[i] = sw.ElapsedMilliseconds;
+                iterationTimes[i] = sw.Elapsed.TotalMilliseconds;

Also applies to: 24-24

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@PhotoLocatorTest/BenchmarkHelper.cs` at line 6, BenchmarkHelper currently
exposes only static members and uses sw.ElapsedMilliseconds which truncates
timing precision; mark class BenchmarkHelper as static to prevent instantiation,
change the iterationTimes array to double[] (or long[] if you prefer ticks) and
replace uses of sw.ElapsedMilliseconds with sw.Elapsed.TotalMilliseconds (or
sw.ElapsedTicks) and update all related method signatures/returns that reference
iterationTimes or its element type (e.g., any methods that build/return
iterationTimes) so types align with the new double[]/long[] choice.
PhotoLocator/BitmapOperations/IIRMinMaxOperation.cs (1)

51-51: Nit: typo quaterWidthquarterWidth.

The refactor logic is correct (each of the 4 column states is independent and the tail loop covers the Stride % 4 remainder), but the variable name is misspelled in both MinFilter and MaxFilter.

✏️ Suggested rename
-                    int quaterWidth = plane.Stride / 4;
-                    Parallel.For(0, quaterWidth, x4 =>
+                    int quarterWidth = plane.Stride / 4;
+                    Parallel.For(0, quarterWidth, x4 =>
                     ...
-                    Parallel.For(quaterWidth * 4, plane.Stride, x =>
+                    Parallel.For(quarterWidth * 4, plane.Stride, x =>

Also applies to: 103-103, 174-174, 226-226

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@PhotoLocator/BitmapOperations/IIRMinMaxOperation.cs` at line 51, The variable
name `quaterWidth` is misspelled in IIRMinMaxOperation.cs; rename it to
`quarterWidth` across the file so both MinFilter and MaxFilter use the correct
identifier (update the declaration and all usages in methods MinFilter and
MaxFilter and the other occurrences noted) to keep naming consistent and avoid
confusion or potential shadowing issues.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@PhotoLocator/BitmapOperations/IIRSmoothOperation.cs`:
- Around line 84-92: In IIRSmoothOperation.cs inside the non-AVX Parallel.For
lambda (the block that uses fixed (float* pixels = plane.Elements) and
Vector4.Load), remove the shadowed local declaration "int vectorizedSegments =
plane.Stride / vectorSize;"—it duplicates the outer variable vectorizedSegments
and is unused; simply delete that line so the lambda uses the captured outer
vectorizedSegments variable.
- Around line 47-109: Replace the AVX/Vector4 platform split in
IIRSmoothOperation.cs with a single implementation using
System.Numerics.Vector<float> so the runtime picks the best SIMD (AVX on x64,
NEON on ARM64) and remove duplicated forward/backward loops; specifically,
eliminate the Avx.IsSupported branch and the Vector4 branch and implement one
Parallel.For over vectorizedSegments that loads/stores using Vector<float> (use
Vector<float>.Count to compute vectorSize), apply filterSize and scale via
Vector.Create and Vector.Multiply/Add, and perform the same forward then
backward propagation using that Vector<float> v; also remove the inner shadowing
declaration int vectorizedSegments = plane.Stride / vectorSize that appears
inside the Vector4 branch (it is unused and shadows the outer variable).

In `@PhotoLocatorTest/BenchmarkHelper.cs`:
- Line 30: The Run method in BenchmarkHelper currently ends by unconditionally
throwing AssertFailedException which makes every benchmark call appear as a
failing test; update Run to surface timings without failing tests by replacing
the final throw of AssertFailedException with a non-failing test outcome (e.g.,
call Assert.Inconclusive($"Min={min} ms, median={median} ms") or return a result
object instead) and document the behavior in a summary comment on the
BenchmarkHelper.Run method so callers know benchmarks intentionally report
timing via inconclusive/returned results rather than true failures; reference
the Run method and the thrown AssertFailedException when making the change.
- Around line 16-25: The code calls GC.EndNoGCRegion() unconditionally even
though GC.TryStartNoGCRegion(...) can return false; modify the loop in
BenchmarkHelper (the block using GC.Collect(), GC.TryStartNoGCRegion, Stopwatch
and iterationTimes) to capture the boolean result (e.g., started =
GC.TryStartNoGCRegion(...)), only call GC.EndNoGCRegion() when started is true,
and ensure EndNoGCRegion() is invoked from a finally block if started to
guarantee cleanup on exceptions; if TryStartNoGCRegion returns false, still run
the timing action but skip EndNoGCRegion().

---

Nitpick comments:
In `@PhotoLocator/BitmapOperations/IIRMinMaxOperation.cs`:
- Line 51: The variable name `quaterWidth` is misspelled in
IIRMinMaxOperation.cs; rename it to `quarterWidth` across the file so both
MinFilter and MaxFilter use the correct identifier (update the declaration and
all usages in methods MinFilter and MaxFilter and the other occurrences noted)
to keep naming consistent and avoid confusion or potential shadowing issues.

In `@PhotoLocatorTest/BenchmarkHelper.cs`:
- Line 6: BenchmarkHelper currently exposes only static members and uses
sw.ElapsedMilliseconds which truncates timing precision; mark class
BenchmarkHelper as static to prevent instantiation, change the iterationTimes
array to double[] (or long[] if you prefer ticks) and replace uses of
sw.ElapsedMilliseconds with sw.Elapsed.TotalMilliseconds (or sw.ElapsedTicks)
and update all related method signatures/returns that reference iterationTimes
or its element type (e.g., any methods that build/return iterationTimes) so
types align with the new double[]/long[] choice.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: db4134a1-5d49-4022-a2c3-9c957f41c32e

📥 Commits

Reviewing files that changed from the base of the PR and between fc14fa9 and 54a3927.

📒 Files selected for processing (4)
  • PhotoLocator/BitmapOperations/FloatBitmap.cs
  • PhotoLocator/BitmapOperations/IIRMinMaxOperation.cs
  • PhotoLocator/BitmapOperations/IIRSmoothOperation.cs
  • PhotoLocatorTest/BenchmarkHelper.cs

Comment thread PhotoLocator/BitmapOperations/IIRSmoothOperation.cs Outdated
Comment thread PhotoLocator/BitmapOperations/IIRSmoothOperation.cs Outdated
Comment thread PhotoLocatorTest/BenchmarkHelper.cs
Comment thread PhotoLocatorTest/BenchmarkHelper.cs Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
PhotoLocatorTest/BenchmarkHelper.cs (1)

16-25: ⚠️ Potential issue | 🟠 Major

GC.EndNoGCRegion() still called unconditionally.

GC.TryStartNoGCRegion can return false (e.g., on memory-constrained machines or if a no‑GC region is already active), in which case the unconditional GC.EndNoGCRegion() on line 23 will throw InvalidOperationException and abort the benchmark loop. Capture the return value and only end the region when it actually started. A try/finally around the timed work would also ensure the region is closed if action() throws.

Proposed fix
             for (int i = 0; i < outerIterations; i++)
             {
                 GC.Collect();
-                GC.TryStartNoGCRegion(1024 * 1024 * 100);
-                var sw = Stopwatch.StartNew();
-                for (int j = 0; j < innerLoops; j++)
-                    action();
-                sw.Stop();
-                Console.WriteLine($"Iteration {i + 1}: {sw.ElapsedMilliseconds} ms");
-                GC.EndNoGCRegion();
-                iterationTimes[i] = sw.ElapsedMilliseconds;
+                bool inNoGc = GC.TryStartNoGCRegion(1024 * 1024 * 100);
+                var sw = Stopwatch.StartNew();
+                try
+                {
+                    for (int j = 0; j < innerLoops; j++)
+                        action();
+                }
+                finally
+                {
+                    sw.Stop();
+                    if (inNoGc && GCSettings.LatencyMode == GCLatencyMode.NoGCRegion)
+                        GC.EndNoGCRegion();
+                }
+                Console.WriteLine($"Iteration {i + 1}: {sw.ElapsedMilliseconds} ms");
+                iterationTimes[i] = sw.ElapsedMilliseconds;
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@PhotoLocatorTest/BenchmarkHelper.cs` around lines 16 - 25, GC.EndNoGCRegion()
is being called unconditionally which can throw if GC.TryStartNoGCRegion(...)
returned false; modify the loop around the benchmark so you capture the boolean
result of GC.TryStartNoGCRegion(1024 * 1024 * 100) into a variable (e.g.,
started), wrap the timed work (Stopwatch sw, the for loop calling action() and
recording sw.ElapsedMilliseconds into iterationTimes) in a try/finally, and only
call GC.EndNoGCRegion() in the finally when started is true so you always end
the region if you started it but never call EndNoGCRegion when
TryStartNoGCRegion failed.
🧹 Nitpick comments (1)
PhotoLocator/BitmapOperations/IIRSmoothOperation.cs (1)

45-46: Redundant unsafe block.

Line 19 already opens an unsafe block, so the nested unsafe on line 45 is superfluous and only adds indentation. Consider removing it for consistency with the horizontal pass above.

♻️ Proposed simplification
-                // Vertical smooth - vectorized over columns when possible
-                unsafe
-                {
-                    int vectorSize = Vector<float>.Count;
-                    int vectorizedSegments = plane.Stride / vectorSize;
-                    var filterSizeV = Vector.Create(filterSize);
-                    var scaleV = Vector.Create(scale);
-                    Parallel.For(0, vectorizedSegments, vi =>
-                    {
-                        ...
-                    });
-                    
-                    // Remaining columns - columns not divisible by vectorSize
-                    Parallel.For(vectorizedSegments * vectorSize, plane.Stride, x =>
-                    {
-                        ...
-                    });
-                }
+                // Vertical smooth - vectorized over columns when possible
+                int vectorSize = Vector<float>.Count;
+                int vectorizedSegments = plane.Stride / vectorSize;
+                var filterSizeV = Vector.Create(filterSize);
+                var scaleV = Vector.Create(scale);
+                Parallel.For(0, vectorizedSegments, vi =>
+                {
+                    ...
+                });
+
+                // Remaining columns - columns not divisible by vectorSize
+                Parallel.For(vectorizedSegments * vectorSize, plane.Stride, x =>
+                {
+                    ...
+                });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@PhotoLocator/BitmapOperations/IIRSmoothOperation.cs` around lines 45 - 46,
The nested unsafe block inside the IIRSmoothOperation vertical pass is redundant
because an outer unsafe is already in scope; remove the inner "unsafe { ... }"
wrapper and de-indent its contents so the code inside continues to execute under
the existing outer unsafe block (locate the nested unsafe in the
IIRSmoothOperation class and collapse it, mirroring how the horizontal pass is
formatted).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@PhotoLocatorTest/BenchmarkHelper.cs`:
- Around line 16-25: GC.EndNoGCRegion() is being called unconditionally which
can throw if GC.TryStartNoGCRegion(...) returned false; modify the loop around
the benchmark so you capture the boolean result of GC.TryStartNoGCRegion(1024 *
1024 * 100) into a variable (e.g., started), wrap the timed work (Stopwatch sw,
the for loop calling action() and recording sw.ElapsedMilliseconds into
iterationTimes) in a try/finally, and only call GC.EndNoGCRegion() in the
finally when started is true so you always end the region if you started it but
never call EndNoGCRegion when TryStartNoGCRegion failed.

---

Nitpick comments:
In `@PhotoLocator/BitmapOperations/IIRSmoothOperation.cs`:
- Around line 45-46: The nested unsafe block inside the IIRSmoothOperation
vertical pass is redundant because an outer unsafe is already in scope; remove
the inner "unsafe { ... }" wrapper and de-indent its contents so the code inside
continues to execute under the existing outer unsafe block (locate the nested
unsafe in the IIRSmoothOperation class and collapse it, mirroring how the
horizontal pass is formatted).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4fa8911a-b8d4-4d70-8ac1-7a9d0abcd0d0

📥 Commits

Reviewing files that changed from the base of the PR and between 54a3927 and 56420da.

📒 Files selected for processing (3)
  • PhotoLocator/BitmapOperations/IIRMinMaxOperation.cs
  • PhotoLocator/BitmapOperations/IIRSmoothOperation.cs
  • PhotoLocatorTest/BenchmarkHelper.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • PhotoLocator/BitmapOperations/IIRMinMaxOperation.cs

@meesoft meesoft merged commit da158e6 into main Apr 27, 2026
5 checks passed
@meesoft meesoft deleted the features/OptimizeLocalContrast branch April 27, 2026 17:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant